﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Text;

namespace REPLACEBYTES
{
    class Program
    {
        const int ERRORLEVEL_SUCCESS = 0;
        const int ERRORLEVEL_WRONG_PARAM = 1;
        const int ERRORLEVEL_WORK_FILE = 2;
        const int ERRORLEVEL_WORK_REPLACE = 3;
        const Int64 DEF_CHUNK = 102400; //default value for chunk 100KB
        const Int64 MAX_CHUNK = 10485760; //max value for chunk 10MB
        static bool arg_leave_original_file = false;
        static bool arg_verbose_information = false;
        static Int64 arg_chunk = DEF_CHUNK;
        static string arg_file_path = "";
        static string temp_filename = "";
        static byte[] arg_find_what;
        static byte[] arg_replace_with;
        static FileStream Source, Destination;
        static long file_len;
        static long total_replaces = 0;


        static void ShouwFullHelp()
        {
            Console.WriteLine("");
            Console.WriteLine("REPLACEBYTES.EXE - Replaces byte sequence with another byte sequence.");
            Console.WriteLine("");
            Console.WriteLine("");
            Console.WriteLine("Usage:");
            Console.WriteLine("");
            Console.WriteLine("REPLACEBYTES.EXE [-L] [-V] [-C=chunk] -S=[drive:][path]filename -F=find_what -R=replace_with");
            Console.WriteLine("");
            Console.WriteLine("");
            Console.WriteLine("Parameters:");
            Console.WriteLine("");
            Console.WriteLine(" -L - Leave source file with new extension .old, else source file will be removed.");
            Console.WriteLine(" -V - Verbose information.");
            Console.WriteLine(" -C=chunk - The Chunk of file proceeding (in B,KB,MB, max {0}MB, default {1}KB).", MAX_CHUNK / 1048576, DEF_CHUNK / 1024);
            Console.WriteLine(" -S=[drive:][path]filename - Specifies the file for proceeding.");
            Console.WriteLine(" -F=find_what - Byte sequence in HEX format to be found and replaced.");
            Console.WriteLine(" -R=replace_with - Byte sequence in HEX format for replacement.");
            Console.WriteLine("");
            Console.WriteLine("");
            Console.WriteLine("Return Value:");
            Console.WriteLine("");
            Console.WriteLine("ERRORLEVEL = 0 - successfull.");
            Console.WriteLine("ERRORLEVEL = 1 - no argumets passed or wrong parameter usage.");
            Console.WriteLine("ERRORLEVEL = 2 - [drive:][path]filename could not be find.");
            Console.WriteLine("ERRORLEVEL = 3 - Error data proceeding.");
            Console.WriteLine("");
            Console.WriteLine("");
            Console.WriteLine("Example:");
            Console.WriteLine("");
            Console.WriteLine("REPLACEBYTES.EXE -C=200KB -S=c:\\temp\\test01.txt -F=0A -R=0A0D");
        }


        static int ParseeParameters(string[] args)
        {
            /// <summary>
            /// Show detailed desctription of application usage.
            /// </summary>
            for (int i = 0; i < args.Length; i++)
            {
                args[i] = args[i].Replace("/", "-");
            }
            if (args.Length == 0)
            {
                ShouwFullHelp();
                return ERRORLEVEL_WRONG_PARAM;
            }
            if (args.Length == 1)
                if (args[0].ToUpper() == "-H")
                {
                    ShouwFullHelp();
                    return ERRORLEVEL_WRONG_PARAM;
                }
            bool parameter_exists_L = false;
            bool parameter_exists_V = false;
            bool parameter_exists_C = false;
            bool parameter_exists_S = false;
            bool parameter_exists_F = false;
            bool parameter_exists_R = false;
            foreach (string arg in args)
            {
                if (arg.Length == 2)
                {
                    if (arg.Substring(0, 2).ToUpper() == "-L")
                    {
                        parameter_exists_L = true;
                        arg_leave_original_file = true;
                    }
                    if (arg.Substring(0, 2).ToUpper() == "-V")
                    {
                        parameter_exists_V = true;
                        arg_verbose_information = true;
                    }
                }
                if (arg.Length >= 3)
                {
                    if (arg.Substring(0, 3).ToUpper() == "-S=")
                    {
                        parameter_exists_S = true;
                        try
                        {
                            arg_file_path = Path.GetFullPath(arg.Substring(3, arg.Length - 3));
                            temp_filename = arg_file_path + ".tmp";
                        }
                        catch (Exception e)
                        {
                            Console.WriteLine("Error in \"-S\" parameter: \"{0}\", \"{1}\"", arg, e.Message);
                            return ERRORLEVEL_WRONG_PARAM;
                        }
                    }
                    if (arg.Substring(0, 3).ToUpper() == "-F=")
                    {
                        parameter_exists_F = true;
                        try
                        {
                            string str01 = arg.Substring(3, arg.Length - 3).ToUpper();
                            arg_find_what = new byte[str01.Length / 2];
                            for (int i = 0; i < str01.Length / 2; i++)
                            {
                                arg_find_what[i] = byte.Parse(str01.Substring(2 * i, 2), System.Globalization.NumberStyles.AllowHexSpecifier);
                            }
                        }
                        catch (Exception e)
                        {
                            Console.WriteLine("Error in \"-F\" parameter: \"{0}\", \"{1}\"", arg, e.Message);
                            return ERRORLEVEL_WRONG_PARAM;
                        }
                    }
                    if (arg.Substring(0, 3).ToUpper() == "-R=")
                    {
                        parameter_exists_R = true;
                        try
                        {
                            string str01 = arg.Substring(3, arg.Length - 3).ToUpper();
                            arg_replace_with = new byte[str01.Length / 2];
                            for (int i = 0; i < str01.Length / 2; i++)
                            {
                                arg_replace_with[i] = byte.Parse(str01.Substring(2 * i, 2), System.Globalization.NumberStyles.AllowHexSpecifier);
                            }
                        }
                        catch (Exception e)
                        {
                            Console.WriteLine("Error in \"-R\" parameter: \"{0}\", \"{1}\"", arg, e.Message);
                            return ERRORLEVEL_WRONG_PARAM;
                        }
                    }
                    if (arg.Substring(0, 3).ToUpper() == "-C=")
                    {
                        parameter_exists_C = true;
                        if (arg.Substring(arg.Length - 2, 2).ToUpper() == "MB")
                        {
                            try
                            {
                                arg_chunk = Int64.Parse(arg.Substring(3, arg.Length - 5));
                                arg_chunk = arg_chunk * 1048576; //MB -> BYTE
                                if (arg_chunk > MAX_CHUNK)
                                    arg_chunk = MAX_CHUNK;
                            }
                            catch (Exception e)
                            {
                                Console.WriteLine("Error in \"-C\" parameter: {0}, \"{1}\"", arg, e.Message);
                                return ERRORLEVEL_WRONG_PARAM;
                            }
                        }
                        else
                        {
                            if (arg.Substring(arg.Length - 2, 2).ToUpper() == "KB")
                            {
                                try
                                {
                                    arg_chunk = Int64.Parse(arg.Substring(3, arg.Length - 5));
                                    arg_chunk = arg_chunk * 1024; //KB -> BYTE
                                    if (arg_chunk > MAX_CHUNK)
                                        arg_chunk = MAX_CHUNK;




                                }
                                catch (Exception e)
                                {
                                    Console.WriteLine("Error in \"-C\" parameter: {0}, \"{1}\"", arg, e.Message);
                                    return ERRORLEVEL_WRONG_PARAM;
                                }
                            }
                            else
                            {
                                if (arg.Substring(arg.Length - 1, 1).ToUpper() == "B")
                                {
                                    try
                                    {
                                        arg_chunk = Int64.Parse(arg.Substring(3, arg.Length - 4));
                                        if (arg_chunk > MAX_CHUNK)
                                            arg_chunk = MAX_CHUNK;
                                    }
                                    catch (Exception e)
                                    {
                                        Console.WriteLine("Error in \"-C\" parameter: {0}, \"{1}\"", arg, e.Message);
                                        return ERRORLEVEL_WRONG_PARAM;
                                    }
                                }
                            }
                        }
                    }
                }
            }
            if ((parameter_exists_S == false))
            {
                Console.WriteLine("Specify \"-S\" parameter, read help REPLACEBYTES.EXE /h");
                return ERRORLEVEL_WRONG_PARAM;
            }
            if ((parameter_exists_F == false))
            {
                Console.WriteLine("Specify \"-F\" parameter, read help REPLACEBYTES.EXE /h");
                return ERRORLEVEL_WRONG_PARAM;
            }
            if ((parameter_exists_R == false))
            {
                Console.WriteLine("Specify \"-R\" parameter, read help REPLACEBYTES.EXE /h");
                return ERRORLEVEL_WRONG_PARAM;
            }
            if (arg_verbose_information == true)
            {
                Console.WriteLine();
                Console.WriteLine("Parameters are:");
                Console.WriteLine();
                Console.WriteLine("-L={0}", arg_leave_original_file.ToString());
                Console.WriteLine("-V={0}", arg_verbose_information.ToString());
                Console.WriteLine("-C={0}B", arg_chunk);
                Console.WriteLine("-S={0}", arg_file_path);
                Console.Write("-F=");
                foreach (byte b in arg_find_what)
                    if (b < 15)
                        Console.Write("0{0:X}", b);
                    else
                        Console.Write("{0:X}", b);
                Console.WriteLine();
                Console.Write("-R=");
                foreach (byte b in arg_replace_with)
                    if (b < 15)
                        Console.Write("0{0:X}", b);
                    else
                        Console.Write("{0:X}", b);
                Console.WriteLine();
                Console.WriteLine();
                Console.WriteLine();
            }




            return ERRORLEVEL_SUCCESS;
        }


        static int SearchBufferPos(ref byte[] buffer, ref byte[] search)
        {
            return SearchBufferPos(ref buffer, ref search, 0);
        }


        static int SearchBufferPos(ref byte[] buffer, ref byte[] search, int start)
        {
            int pos = -1;




            if (start < 0)
                start = 0;




            if ((buffer == null) || (buffer.Length <= 0))
                return pos;




            if ((search == null) || (search.Length <= 0))
                return pos;




            int delta = buffer.Length - search.Length;




            if (start > delta)
                return pos;




            for (int i = start; i <= delta; i++)
            {
                //if at least first search byte equal to current buffer byte the begin compare
                if (buffer[i] == search[0])
                {
                    bool found = true;




                    for (int j = 0; j < search.Length; j++)
                    {
                        if (buffer[i + j] != search[j])
                        {
                            found = false;
                            j = search.Length;
                        }
                    }




                    if (found)
                        return i;
                }
            }








            return pos;
        }


        static int SearchBufferNrs(ref byte[] buffer, ref byte[] search, ref byte[] trailer, bool lastsearch)
        {
            return SearchBufferNrs(ref buffer, ref search, ref trailer, 0, lastsearch);
        }
        /*lastsearch - last reading of the file*/
        static int SearchBufferNrs(ref byte[] buffer, ref byte[] search, ref byte[] trailer, int start, bool lastsearch)
        {




            //??? Check buffer trailer - if last part is less then seach, it shoulb be saved
            trailer = null;








            int nrs = 0;




            int currpos = start;
            int newpos;// = SearchBufferPos(buffer, search, currpos);
            while ((newpos = SearchBufferPos(ref buffer, ref search, currpos)) != -1)
            {
                currpos = newpos + search.Length;
                if (currpos > buffer.Length - search.Length)
                {
                    if (lastsearch)
                        trailer = new byte[0];
                    else
                    {
                        trailer = new byte[buffer.Length - currpos];
                        for (int i = 0; i < trailer.Length; i++)
                        {
                            trailer[i] = buffer[currpos + i];
                        }
                    }
                }




                nrs++;
            }




            //???think about if it will be last step of file reading than trailer should be 0 length
            if (trailer == null)
                if (lastsearch)
                    trailer = new byte[0];
                else
                {
                    trailer = new byte[search.Length - 1];
                    int j = buffer.Length - 1;
                    for (int i = trailer.Length - 1; i >= 0; i--)
                    {
                        trailer[i] = buffer[j];
                        j--;
                    }
                }




            return nrs;
        }


        static byte[] Replace(ref byte[] buffer, ref byte[] search, ref byte[] replace, ref byte[] trailer, bool lastbuffer)
        {
            byte[] result = null;
            trailer = new byte[0];
            int nrs = 0;




            nrs = SearchBufferNrs(ref buffer, ref search, ref trailer, lastbuffer);

            total_replaces += nrs;


            result = new byte[buffer.Length - (nrs * search.Length) + (nrs * replace.Length) - trailer.Length];




            // inf search string eas not find
            if (nrs == 0)
            {
                Array.Copy(buffer, result, result.Length);
                return result;
            }




            int currpos_buffer = 0;
            int currpos_result = 0;
            int currpos = -1;




            for (int i = 0; i < nrs; i++)
            {
                currpos = SearchBufferPos(ref buffer, ref search, currpos_buffer);
                if (currpos != -1)
                {
                    if (currpos > currpos_buffer)
                    {
                        Array.Copy(buffer, currpos_buffer, result, currpos_result, currpos - currpos_buffer);
                        currpos_result += currpos - currpos_buffer;
                    }
                    Array.Copy(replace, 0, result, currpos_result, replace.Length);




                    currpos_buffer = currpos + search.Length;




                    currpos_result += replace.Length;
                }
                else
                {


                }
            }








            if (result.Length > currpos_result)
                Array.Copy(buffer, currpos_buffer, result, currpos_result, result.Length - currpos_result);












            return result;
        }


        static int OpenFilesForProceeding()
        {
            int result = ERRORLEVEL_SUCCESS;
            //Try to open source file for erading
            try
            {
                Source = File.OpenRead(arg_file_path);
                if (arg_verbose_information)
                {
                    Console.WriteLine("File opened for reading: {0}", arg_file_path);
                    Console.WriteLine();
                }
            }
            catch (Exception e)
            {
                Console.WriteLine("Error opening file {0}: {1}", arg_file_path, e.Message);
                return ERRORLEVEL_WORK_FILE;
            }


            //Check if temp file exists and delete it
            try
            {
                if (File.Exists(temp_filename))
                {
                    if (arg_verbose_information)
                    {
                        Console.WriteLine("Temp file exists: {0}", temp_filename);
                        Console.WriteLine();
                    }
                    File.Delete(temp_filename);
                    if (arg_verbose_information)
                    {
                        Console.WriteLine("Temp file deleted: {0}", temp_filename);
                        Console.WriteLine();
                    }
                }
            }
            catch (Exception e)
            {
                Console.WriteLine("Error deleting temp file {0}: {1}", temp_filename, e.Message);
                return ERRORLEVEL_WORK_FILE;
            }
            //Open (Create) temp file for writing
            try
            {
                Destination = File.OpenWrite(temp_filename);
                if (arg_verbose_information)
                {
                    Console.WriteLine("Temp file created: {0}", temp_filename);
                    Console.WriteLine();
                }
            }
            catch (Exception e)
            {
                Console.WriteLine("Error opening temp file {0}: {1}", temp_filename, e.Message);
                return ERRORLEVEL_WORK_FILE;
            }
            return result;
        }


        static int CloseFilesAndRenameFiles()
        {
            int result = ERRORLEVEL_SUCCESS;
            try
            {
                Destination.Close();
                if (arg_verbose_information)
                {
                    Console.WriteLine("Temp file closed: {0}", temp_filename);
                    Console.WriteLine();
                }
            }
            catch (Exception e)
            {
                Console.WriteLine("Error closing temp file {0}: {1}", temp_filename, e.Message);
                return ERRORLEVEL_WORK_FILE;
            }




            try
            {
                Source.Close();
                if (arg_verbose_information)
                {
                    Console.WriteLine("File closed: {0}", arg_file_path);
                    Console.WriteLine();
                }
            }
            catch (Exception e)
            {
                Console.WriteLine("Error closing file {0}: {1}", arg_file_path, e.Message);
                return ERRORLEVEL_WORK_FILE;
            }
            string month;
            string day;
            string hour;
            string min;
            string sec;
            if (DateTime.Now.Month < 10)
                month = "0" + DateTime.Now.Month.ToString();
            else
                month = DateTime.Now.Month.ToString();
            if (DateTime.Now.Day < 10)
                day = "0" + DateTime.Now.Day.ToString();
            else
                day = DateTime.Now.Day.ToString();
            if (DateTime.Now.Hour < 10)
                hour = "0" + DateTime.Now.Hour.ToString();
            else
                hour = DateTime.Now.Hour.ToString();
            if (DateTime.Now.Minute < 10)
                min = "0" + DateTime.Now.Minute.ToString();
            else
                min = DateTime.Now.Minute.ToString();
            if (DateTime.Now.Second < 10)
                sec = "0" + DateTime.Now.Second.ToString();
            else
                sec = DateTime.Now.Second.ToString();
            string date = DateTime.Now.Year.ToString() + month + day + hour + min + sec;
            if (arg_leave_original_file)
            {
                try
                {
                    File.Move(arg_file_path, arg_file_path + ".old." + date);
                    if (arg_verbose_information)
                    {
                        Console.WriteLine("File renamed {0} to {1}", arg_file_path, arg_file_path + ".old." + date);
                        Console.WriteLine();
                    }
                }
                catch (Exception e)
                {
                    Console.WriteLine("Error renaming original file {0} to {1}: {2}", arg_file_path, arg_file_path + ".old." + date, e.Message);
                    return ERRORLEVEL_WORK_FILE;
                }
            }
            else
            {
                try
                {
                    File.Delete(arg_file_path);
                    if (arg_verbose_information)
                    {
                        Console.WriteLine("File removed: {0}", arg_file_path);
                        Console.WriteLine();
                    }
                }
                catch (Exception e)
                {
                    Console.WriteLine("Error deleting original file {0}: {2}", arg_file_path, e.Message);
                    return ERRORLEVEL_WORK_FILE;
                }
            }
            try
            {
                File.Move(temp_filename, arg_file_path);
                if (arg_verbose_information)
                {
                    Console.WriteLine("File renamed {0} to {1}", temp_filename, arg_file_path);
                    Console.WriteLine();
                }
            }
            catch (Exception e)
            {
                Console.WriteLine("Error renaming original file {0} to {1}: {2}", arg_file_path, arg_file_path + ".old." + date, e.Message);
                return ERRORLEVEL_WORK_FILE;
            }
            return result;
        }


        static int ProceedData()
        {
            int result = ERRORLEVEL_SUCCESS;


            try
            {


                if (arg_verbose_information)
                {
                    Console.WriteLine("");
                    Console.WriteLine("Progress:");
                    Console.WriteLine("|==========|");
                    Console.Write(" ");
                }


                int actual_chunk = 0;
                long read_bytes = 0;
                byte[] trailer = new byte[0];
                long progress = 0;
                long progress_prev = 0;
                do
                {
                    byte[] buffer = new byte[arg_chunk + trailer.Length];
                    byte[] replaced_buffer = new byte[0];
                    Array.Copy(trailer, buffer, trailer.Length);
                    actual_chunk = Source.Read(buffer, trailer.Length, buffer.Length - trailer.Length);
                    if (actual_chunk > 0)
                    {
                        read_bytes += actual_chunk;
                        if (actual_chunk < arg_chunk)
                        {
                            byte[] last_read_buffer = new byte[actual_chunk + trailer.Length];
                            Array.Copy(buffer, last_read_buffer, last_read_buffer.Length);
                            buffer = null;
                            GC.Collect();
                            GC.WaitForPendingFinalizers();
                            buffer = new byte[last_read_buffer.Length];
                            Array.Copy(last_read_buffer, buffer, buffer.Length);
                            last_read_buffer = null;
                            GC.Collect();
                            GC.WaitForPendingFinalizers();
                        }


                        if (read_bytes == file_len)
                            replaced_buffer = Replace(ref buffer, ref arg_find_what, ref arg_replace_with, ref trailer, true);
                        else
                            replaced_buffer = Replace(ref buffer, ref arg_find_what, ref arg_replace_with, ref trailer, false);
                        Destination.Write(replaced_buffer, 0, replaced_buffer.Length);




                        if (arg_verbose_information)
                        {
                            progress = (read_bytes * 10) / file_len;
                            if (progress - progress_prev > 0)
                            {
                                for (int i = 0; i < progress - progress_prev; i++)
                                    Console.Write("*");
                                progress_prev = progress;
                            }
                        }
                    }




                    buffer = null;
                    replaced_buffer = null;
                    GC.Collect();
                    GC.WaitForPendingFinalizers();
                }
                while (actual_chunk > 0);
                if (arg_verbose_information)
                {
                    Console.WriteLine("");
                    Console.WriteLine("");
                }
            }
            catch (Exception e)
            {
                Console.WriteLine();
                Console.WriteLine("Error proceeding data: {0}", e.Message);
                return ERRORLEVEL_WORK_REPLACE;
            }


            return result;
        }


        static int Main(string[] args)
        {
            int result = ERRORLEVEL_SUCCESS;
            result = ParseeParameters(args);
            if (result != ERRORLEVEL_SUCCESS)
                return result;
            result = OpenFilesForProceeding();
            if (result != ERRORLEVEL_SUCCESS)
                return result;




            file_len = Source.Length;
            bool skip_proceeding = false;
            if (arg_chunk > file_len)
                arg_chunk = file_len;
            if (arg_chunk < arg_find_what.Length)
            {
                arg_chunk = arg_find_what.Length;
                if (arg_chunk > file_len)
                {
                    if (arg_verbose_information)
                        Console.WriteLine("The parametr /F is longer then the file length. File proceeding skiped for {0}", arg_file_path);


                    skip_proceeding = true;
                }
            }


            if (!skip_proceeding)
            {
                result = ProceedData();
                if (result != ERRORLEVEL_SUCCESS)
                    return result;
            }


            result = CloseFilesAndRenameFiles();
            if (result != ERRORLEVEL_SUCCESS)
                return result;

            Console.WriteLine("File was proceeded successfully with total {0} replaces /F to /R", total_replaces.ToString());
            //Console.ReadLine();
            return ERRORLEVEL_SUCCESS;
        }
    }
}
